Thursday, January 21, 2021

Sharing available renderings in multisite SXA

Sitecore SXA available renderings

We have a multisite setup in SXA to easily create (small) websites and have been sharing a lot of configuration, layout information and content between those. Check my latest SXA blog posts for more information.

We did already cover sharing placeholder settings. But if we can share those, we might as well share the available renderings configuration. This is a list of renderings that is defined per site to define the availability within the SXA Toolbox - and since SXA 1.8 we can also define the structure of the toolbox with these Available renderings. As explained in the documentation, you have to check the box "Group renderings in sections according to Available Renderings items in the site" to do so.


I believe it is a best practice to do this so that feature is always on in my projects.

But let's get back to sharing...  We have a shared site (and master site) setup, so it would be a good idea to define the available renderings in the shared site and not in every site (created from the master site). This idea was on my radar and I got triggered by Toby Gutierrez to really try this now.


Sharing available renderings

I am going to try the same principle I used for the placeholder settings: if the current site has no configuration in place -meaning no children underneath the available renderings parent node- we check the shared site(s) and get the configuration there.

The toolbox

First of all, we need to figure out how the toolbox works. There is a handler (sxa-toolbox-feed.ashx) but that is not to much use for us here as it works with data that is retrieved elsewhere. I noticed however that in case the renderings grouping is turned on, a AvailableRenderingsOrderingService is called. This service will read the configuration and use this to structure the toolbox data that was passed along. As a side affect, it will also filter (again) on the available renderings.

This means that if you are using the grouping -as I do- you will have to override this. Let's do that:
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.DependencyInjection;
using Sitecore.XA.Foundation.Editing.Models;
using Sitecore.XA.Foundation.Multisite;
using Sitecore.XA.Foundation.Presentation;
using Sitecore.XA.Foundation.SitecoreExtensions.Extensions;
using Sitecore.XA.Foundation.SitecoreExtensions.Repositories;

namespace Feature.Multisite.Editing
{
  public class AvailableRenderingsOrderingService : Sitecore.XA.Foundation.Editing.Service.AvailableRenderingsOrderingService
  {
    public AvailableRenderingsOrderingService(
      IPresentationContext presentationContext,
      IMultisiteContext multisiteContext,
      IContentRepository contentRepository) : base(presentationContext, multisiteContext, contentRepository)
    {
    }

    public override IList<AvailableRenderingEntry> GetOrderedRenderings(Item siteItem, IList<Item> renderings = null)
    {
      var orderDictionary = new Dictionary<ID, ValueTuple<int, string>>();
      var presentationItem = PresentationContext.GetPresentationItem(siteItem);
      var folder = presentationItem?.FirstChildInheritingFrom(Sitecore.XA.Foundation.Presentation.Templates.AvailableRenderingsFolder.ID);
      if (folder != null)
      {
        if (!folder.Children.Any())
        {
          var shared = ServiceLocator.ServiceProvider.GetService<ISharedSitesContext>().GetSharedSitesWithoutCurrent(siteItem);
          foreach (var site in shared)
          {
            presentationItem = PresentationContext.GetPresentationItem(site);
            folder = presentationItem?.FirstChildInheritingFrom(Sitecore.XA.Foundation.Presentation.Templates.AvailableRenderingsFolder.ID);
            if (folder != null && folder.Children.Any())
            {
              break;
            }
          }
        }

        if (folder != null)
        {
          var sections = (CheckboxField)folder.Fields[Sitecore.XA.Foundation.Presentation.Templates.AvailableRenderingsFolder.Fields.GroupRenderingsInSections];
          if (sections != null && sections.Checked)
          {
            var array = folder.Children.Where(item => !string.IsNullOrEmpty(item[Sitecore.XA.Foundation.Presentation.Templates._RenderingsList.Fields.Renderings])).ToArray();
            for (var index1 = 0; index1 < array.Length; ++index1)
            {
              var fieldRenderings = (MultilistField)array[index1].Fields[Sitecore.XA.Foundation.Presentation.Templates._RenderingsList.Fields.Renderings];
              for (var index2 = 0; index2 < fieldRenderings.TargetIDs.Length; ++index2)
              {
                if (!orderDictionary.ContainsKey(fieldRenderings.TargetIDs[index2]))
                {
                  orderDictionary.Add(fieldRenderings.TargetIDs[index2], new ValueTuple<int, string>((index1 + 1) * 1000 + index2, fieldRenderings.InnerField.Item.DisplayName));
                }
              }
            }
          }
        }
      }

      return renderings?.Select(r => new AvailableRenderingEntry
      {
        RenderingItem = r,
        Order = orderDictionary.ContainsKey(r.ID) ? orderDictionary[r.ID].Item1 : 0,
        SectionName = orderDictionary.ContainsKey(r.ID) ? orderDictionary[r.ID].Item2 : r.Parent.DisplayName
      }).OrderBy(r => r.Order).ToList();
    }
  }
}  
This code is almost identical as the original one. The difference is in the GetOrderedRenderings function where we check the existance of a local configuration (folder.Children.Any) and try the shared sites (using the SharedSitesContext) if needed.

The service is registered in the config, so we patch that config to include our version:
<sitecore>
  <services>
    <register serviceType="Sitecore.XA.Foundation.Editing.Service.IAvailableRenderingsOrderingService, Sitecore.XA.Foundation.Editing"
              implementationType="Sitecore.XA.Foundation.Editing.Service.AvailableRenderingsOrderingService, Sitecore.XA.Foundation.Editing" lifetime="Singleton">
      <patch:attribute name="implementationType">Feature.Multisite.Editing.AvailableRenderingsOrderingService, Feature.Multisite</patch:attribute>
    </register>
  </services>
</sitecore>

That's it. Now we can safely delete all children from the Available renderings node in our sites that use our shared site. The toolbox in those sites will use the configuration defined in the shared site.


But...

Note that this only works if the grouping is enabled. As I always do this, I'm not going to try to implement a solution if it's not. This also means that the toolbox handler might be getting too much renderings as a parameter, but the result is ok so that's fine for me. 

However, if you do require such a solution without the grouping you'll need to check the getPlaceholderRenderings pipeline and especially the GetModuleRenderings processor. This has a function called GetSiteRenderingSources and you'll have to add the same logic there as I used in the AvailableRenderingsOrderingService.


Conclusion

Yet another configuration that we can share across all the created sites...  this makes it easier to handle if we add/remove components that can be used on those sites - we only have to do this on one spot from now on.


No comments:

Post a Comment